package jmine.tec.test.testcase;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jmine.tec.test.TestMessages;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

import org.apache.commons.lang.enums.Enum;
import org.apache.log4j.Logger;
import org.springframework.transaction.TransactionStatus;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.bussobj.PersistableBusinessObject;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BasicDataDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.dao.SystemEntityDAO;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.span.SpanAwareDAO;
import bancosys.tec.persist.span.fat.FatSpanAwareDAO;
import bancosys.tec.persist.validator.NullValidatorFactory;
import bancosys.tec.persist.validator.ValidatorFactory;

/**
 * @created May 23, 2006
 * @author Gustavo Almeida
 * @param <T>
 */
public abstract class AbstractBOBaseTestCase<T extends PersistableBusinessObject> extends EmptyDBTestCase {
    private static final Logger LOG = Logger.getLogger(AbstractBOBaseTestCase.class);

    private static final ValidatorFactory VALIDATOR_FACTORY = new NullValidatorFactory();

    // -- contrutores
    // ------------------------------------------------------------

    /**
     * Constructor
     */
    public AbstractBOBaseTestCase() {
        super();
    }

    /**
     * Constructor
     * 
     * @param name name
     */
    public AbstractBOBaseTestCase(String name) {
        super(name);
    }

    // -- metodos a serem implementados nas especializacoes (obrigatorios)
    /**
     * Returns the size of the test data
     * 
     * @return int
     */
    protected abstract int getTestDataSize();

    /**
     * Fills the bo with data of the given index
     * 
     * @param idx index
     * @param bo business object
     */
    protected abstract void fillData(int idx, T bo);

    /**
     * Compares the bo data with the data of the index, asserting that the values are correct.
     * 
     * @param idx index
     * @param bo business object
     * @throws BeanNotFoundException BeanNotFoundException
     */
    protected abstract void compareData(int idx, T bo) throws BeanNotFoundException;

    /**
     * Método utilizado para salvar o BO criado pelo getSavedTestData(). Pode ser sobrecarregado com a inteção de executar alguma
     * verificação antes de salvar o BO.
     * 
     * @param bo O bo sendo salvo.
     */
    protected abstract void doSaveTestData(T bo);

    /**
     * Returns the list of objects being tested, saved.
     * 
     * @return List of T
     */
    public final List<T> getSavedTestData() {
        List<T> result = new LinkedList<T>();

        Collection<T> collection = this.findAllData();
        if (collection.size() < this.getTestDataSize()) {
            TransactionStatus status = this.getHelper().getTransactionStatus();
            boolean commit = false;
            try {
                Iterator<T> data = this.getUnsavedTestData().iterator();
                while (data.hasNext()) {
                    T bo = data.next();
                    this.doSaveTestData(bo);
                }
                collection = this.findAllData();
                commit = true;
            } finally {
                if (commit) {
                    this.getHelper().commit(status);
                } else {
                    this.getHelper().rollback(status);
                }
            }
        }

        result.addAll(collection);
        return result;
    }

    // Classes que devem ter seus métodos ignorados no teste de sintaxe das queries.
    private static final List<Class<?>> DAOS_COM_QUERIES_IGNORADAS;
    static {
        DAOS_COM_QUERIES_IGNORADAS = new ArrayList<Class<?>>();
        DAOS_COM_QUERIES_IGNORADAS.add(BaseDAO.class);
        DAOS_COM_QUERIES_IGNORADAS.add(BasicDataDAO.class);
        DAOS_COM_QUERIES_IGNORADAS.add(SpanAwareDAO.class);
        DAOS_COM_QUERIES_IGNORADAS.add(FatSpanAwareDAO.class);
        DAOS_COM_QUERIES_IGNORADAS.add(SystemEntityDAO.class);
    }

    // -- metodos a serem implementados nas especializacoes (opcionais)
    // ---------

    /**
     * Preenche o id do bean, deve ser usado em testes que usem beans com id assigned.
     * 
     * @param bo bean.
     */
    protected void preencheIdNosTestes(T bo) {
        // empty
    }

    /**
     * Compara id do bean com o que foi preenchido no {@link #preencheIdNosTestes(PersistableBusinessObject)}, deve ser usado em testes que
     * usem beans com id assigned.
     * 
     * @param bo bean.
     */
    protected void comparaIdNosTestes(T bo) {
        // empty
    }

    /**
     * Test set up. Define as classes que nao devem ser autorizadas.
     * 
     * @throws Exception deixa subir qquer execção que for lançada.
     */
    @Override
    public void setUp() throws Exception {
        super.setUp();
        this.getHelper().getSessionFactory().getBoAuthorizationManager().setExclusions(this.getClassesWithAuthorizationDisabled());
    }

    /**
     * Deve ser sobreescrito pelo teste que quiser desabilitar a autorizacao para determinadas classes.
     * 
     * @return classes que deverao ter sua autrizacao desabilitada.
     */
    protected Collection<Class<? extends Persistable>> getClassesWithAuthorizationDisabled() {
        return new LinkedList<Class<? extends Persistable>>();
    }

    /**
     * Tests get data
     */
    public void testGetTestData() {
        LOG.trace("testGetTestData()");

        TransactionStatus status = this.getHelper().getTransactionStatus();
        try {
            Collection<T> data = this.getSavedTestData();
            assertEquals("Colecao não tem o tamanho correto.", this.getTestDataSize(), data.size());
        } finally {
            this.getHelper().commit(status);
        }
    }

    /**
     * Returns unsave test data
     * 
     * @return List of T
     */
    public List<T> getUnsavedTestData() {
        this.executeInitializeTestData();

        List<T> result = new LinkedList<T>();
        for (int i = 0; i < this.getTestDataSize(); i++) {
            T bo = this.getNewBOInstance();
            this.fillData(i, bo);
            result.add(bo);
        }
        return result;
    }

    /**
     * Returns all entities on the database.
     * 
     * @return Collection of T
     */
    protected Collection<T> findAllData() {
        return this.getDAO().findAllOrderedByPk();
    }

    /**
     * this.getLogger() Creates a new BO instance
     * 
     * @return T
     */
    protected T getNewBOInstance() {
        return this.getDAO().createBean();
    }

    /**
     * Returns the DAO
     * 
     * @return BaseDAO of T
     */
    protected BaseDAO<T> getDAO() {
        return this.getController().getDAOFactory().getGenericDAO(this.getBOClass());
    }

    /**
     * Tests "find" methods on the DAO.
     * 
     * @throws Throwable Throwable
     */
    public void testDAOQuerySyntax() throws Throwable {
        LOG.trace("testDAOQuerySyntax()");

        TransactionStatus status = this.getHelper().getTransactionStatus();

        try {
            BaseDAO<T> dao = this.getDAO();

            Class<?> clazz = dao.getClass();

            for (Method method : clazz.getMethods()) {
                if (DAOS_COM_QUERIES_IGNORADAS.contains(method.getDeclaringClass())) {
                    continue;
                }
                // comparacao !method.isBridge(): não executar método criado pelo compilador para tipos covariantes e generics.
                if (method.getName().startsWith("find") && !method.isBridge()) {
                    this.invokeFindMethod(dao, method);
                }
            }
        } finally {
            this.getHelper().rollback(status);
        }
    }

    /**
     * Invokes find method
     * 
     * @param dao dao
     * @param method method
     */
    protected void invokeFindMethod(BaseDAO<T> dao, Method method) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        Object[] parameters = new Object[parameterTypes.length];

        for (int i = 0; i < parameters.length; i++) {
            parameters[i] = this.instantiateParameter(method, i, parameterTypes[i]);
        }

        try {
            method.invoke(dao, parameters);
        } catch (InvocationTargetException e) {
            Throwable target = (e).getTargetException();
            if (target instanceof BeanNotFoundException) {
                return;
            }
            throw new PersistenceException(TestMessages.ERROR_INVOKING_METHOD.create(method.getName()), e);
        } catch (Throwable e) {
            throw new PersistenceException(TestMessages.ERROR_INVOKING_METHOD.create(method.getName()), e);
        }
    }

    /**
     * Instantiates parameters for method calling
     * 
     * @param method method
     * @param parameterPosition parameterPosition
     * @param parameterType parameterType
     * @return parameter
     */
    @SuppressWarnings({ "unchecked", "deprecation" })
    protected Object instantiateParameter(Method method, int parameterPosition, Class parameterType) {
        if (bancosys.tec.query.QueryParameter.class.isAssignableFrom(parameterType)) {
            bancosys.tec.query.QueryParameter parameter = new bancosys.tec.query.impl.BaseQueryParameter();
            parameter.addBO(this.getNewBOInstance());
            return parameter;
        }

        if (Enum.class.isAssignableFrom(parameterType)) {
            try {
                Method itMethod = parameterType.getMethod("iterator");
                Iterator<?> it = (Iterator<?>) itMethod.invoke(null);
                return it.next();
            } catch (Throwable e) {
                throw new PersistenceException(e);
            }
        }

        if (Long.class.isAssignableFrom(parameterType)) {
            return 1L;
        }

        if (Integer.class.isAssignableFrom(parameterType)) {
            return 1;
        }

        if (Double.class.isAssignableFrom(parameterType)) {
            return 1.0;
        }

        if (Float.class.isAssignableFrom(parameterType)) {
            return 1.0;
        }

        if (BigDecimal.class.isAssignableFrom(parameterType)) {
            return new BigDecimal(1.0);
        }

        if (Boolean.class.isAssignableFrom(parameterType)) {
            return Boolean.FALSE;
        }

        if (Character.class.isAssignableFrom(parameterType)) {
            return 'a';
        }

        if (parameterType.isPrimitive()) {
            if (parameterType == Long.TYPE) {
                return 1L;
            }

            if (parameterType == Integer.TYPE) {
                return 1;
            }

            if (parameterType == Double.TYPE) {
                return 1.0;
            }

            if (parameterType == Float.TYPE) {
                return 1.0;
            }

            if (parameterType == Boolean.TYPE) {
                return Boolean.FALSE;
            }

            if (parameterType == Character.TYPE) {
                return 'c';
            }
        }

        if (parameterType == Persistable.class) {
            return this.getNewBOInstance();
        }

        if (Map.class.isAssignableFrom(parameterType)) {
            return new HashMap();
        }

        if (Set.class.isAssignableFrom(parameterType)) {
            return new HashSet();
        }

        if (List.class.isAssignableFrom(parameterType) || Collection.class.isAssignableFrom(parameterType)) {
            return new ArrayList(0);
        }

        if (Modifier.isAbstract(parameterType.getModifiers())) {
            return null;
        }

        if (Persistable.class.isAssignableFrom(parameterType)) {
            BaseDAO dao = this.getController().getDAOFactory().getDAO(parameterType);
            return dao.createBean();
        }

        if (parameterType.isEnum()) {
            return parameterType.getEnumConstants()[0];
        }

        try {
            return this.createInstance(parameterType);
        } catch (PersistenceException e) {
            return null;
        }
    }

    /**
     * Creates a new instance of the given class
     * 
     * @param clazz class
     * @return new instance of clazz
     */
    protected Object createInstance(Class<?> clazz) {
        return this.createTypeSafeInstance(clazz);
    }

    /**
     * Equivalente a {@link #createInstance(Class)}, porem typesafe
     * 
     * @param <E> o tipo da classe
     * @param clazz Class
     * @return E
     */
    protected <E> E createTypeSafeInstance(Class<E> clazz) {
        try {
            Constructor<E> constructor = clazz.getDeclaredConstructor((Class[]) null);
            if (!Modifier.isPublic(constructor.getModifiers()) || !Modifier.isPublic(constructor.getDeclaringClass().getModifiers())) {
                constructor.setAccessible(true);
            }
            return constructor.newInstance(new Object[0]);
        } catch (InstantiationException e) {
            throw new PersistenceException(TestMessages.UNABLE_TO_INSTANTIATE_PARAMETER.create(clazz.getName()), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(TestMessages.UNABLE_TO_INSTANTIATE_PARAMETER.create(clazz.getName()), e);
        } catch (SecurityException e) {
            throw new PersistenceException(TestMessages.UNABLE_TO_INSTANTIATE_PARAMETER.create(clazz.getName()), e);
        } catch (NoSuchMethodException e) {
            throw new PersistenceException(TestMessages.UNABLE_TO_INSTANTIATE_PARAMETER.create(clazz.getName()), e);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(TestMessages.UNABLE_TO_INSTANTIATE_PARAMETER.create(clazz.getName()), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(TestMessages.UNABLE_TO_INSTANTIATE_PARAMETER.create(clazz.getName()), e);
        }

    }

    /**
     * Initializes test data
     * 
     * @throws BeanNotFoundException BeanNotFoundException
     */
    @Override
    protected void initializeTestData() throws BeanNotFoundException {
        // VAZIO
    }

    /**
     * Devolve a validarion factory.
     * 
     * @return a validarion factory.
     */
    protected ValidatorFactory getValidatorFactory() {
        return VALIDATOR_FACTORY;
    }

    /**
     * Devolve a classe gerenciada pelo DAO.
     * 
     * @return a classe gerenciada pelo DAO.
     */
    @SuppressWarnings("unchecked")
    protected Class<? extends T> getBOClass() {
        try {
            TypeVariable<Class<AbstractBOBaseTestCase>> typeParameter = AbstractBOBaseTestCase.class.getTypeParameters()[0];
            return (Class<T>) GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType(typeParameter, this.getClass());
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(TestMessages.CLASS_DOES_NOT_IMPLEMENT_GENERICS_CORRECTLY.create(this.getClass()), e);
        }
    }
}
